package com.kedacom.ctsp.authz.oauth2.provider;

import com.kedacom.ctsp.authz.AuthenticationService;
import com.kedacom.ctsp.authz.access.DataAccessHandler;
import com.kedacom.ctsp.authz.entity.AuthContext;
import com.kedacom.ctsp.authz.entity.AuthResource;
import com.kedacom.ctsp.authz.entity.Authentication;
import com.kedacom.ctsp.authz.entity.AuthzTypeEnum;
import com.kedacom.ctsp.authz.exception.UnauthorizedException;
import com.kedacom.ctsp.authz.oauth2.core.AuthzConstants;
import com.kedacom.ctsp.authz.security.AuthzSecurityProperties;
import com.kedacom.ctsp.orm.param.Param;
import com.kedacom.ctsp.web.aop.context.MethodInterceptorContext;
import com.kedacom.ctsp.web.aop.context.MethodInterceptorHolder;
import com.kedacom.ctsp.web.controller.message.ResponseMessage;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.*;

/**
 * @Remark 作用是针对url做权限拦截，控制
 * @Author fenghaiju
 * @Date 2018-07-18
 */
@Aspect
@Component
@Slf4j
@Order(0)
@ConditionalOnProperty(prefix = "commons.authz.urls.dateAccess", value = "enable", matchIfMissing = true)
public class AuthzAspectHandler {
    @Autowired
    protected List<DataAccessHandler> dataAccessHandlers;
    @Autowired
    protected AuthenticationService authenticationService;
    @Autowired
    private AuthzSecurityProperties authzSecurityProperties;

    /**
     * execution(方法修饰符(可选)  返回类型  方法名  参数  异常模式(可选))
     * 修饰符 通配符(*表示所有，可定义void\String 返回值为具体类型的方法) 包名 类名 方法名 参数
     * 定义AOP扫描的路径，第一个路径只扫描controller中的一个方法，两个点表示任何参数
     */
    @Pointcut("execution( * com.kedacom.ctsp.*.controller.*.*(..))")
    public void authz() {

    }

    @Pointcut("execution( com.kedacom.ctsp.web.controller.message.ResponseMessage com.kedacom.ctsp.*.controller.*.*(..))")
    public void responses() {

    }

    /**
     * 在方法执行之前,使用url判断的方式，根据当前登录人来判断数据权限
     */
    @Before(value = "authz()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        //判断是否是url和拥有resource
        if (!urlFilter(request)) {
            if (null != request.getAttribute(AuthzConstants.AUTHZ_URL_CONTEXT) && null != request.getAttribute(AuthzConstants.AUTH_RESOURCE_CONTEXT)) {
                AuthzTypeEnum authzTypeEnum = (AuthzTypeEnum) request.getAttribute(AuthzConstants.AUTHZ_URL_CONTEXT);
                if (authzTypeEnum == AuthzTypeEnum.URI) {
                    //获取拦截器判断权限的资源
                    List<AuthResource> authResources = (List<AuthResource>) request.getAttribute(AuthzConstants.AUTH_RESOURCE_CONTEXT);

                    AuthContext context = setAuthzContext(joinPoint);
                    getAuthorized(context, authResources);
                }
            }
        }
    }

    /**
     * 获取返回内容，进行过滤
     *
     * @param responseMessage
     */
    @AfterReturning(returning = "responseMessage", pointcut = "responses()")
    public void doAfterReturn(JoinPoint joinpoint, ResponseMessage responseMessage) {
        setFields(joinpoint, responseMessage);
    }

    /**
     * 返回显示字段
     *
     * @param joinPoint
     * @param responseMessage
     */
    private void setFields(JoinPoint joinPoint, ResponseMessage responseMessage) {
        AuthContext context = setAuthzContext(joinPoint);
        Set<String> tempIncludes = new HashSet<>();
        Set<String> tempExcludes = new HashSet<>();

        context.getParamContext().getParameter(Param.class).ifPresent(
                param -> {
                    tempIncludes.addAll(param.getIncludes());
                    tempExcludes.addAll(param.getExcludes());
                }
        );
        if (CollectionUtils.isNotEmpty(tempIncludes)) {
            // 设置可以显示的字段
            responseMessage.setFields(new LinkedHashSet<>(CollectionUtils.removeAll(tempIncludes, tempExcludes)));

        }
    }

    /***
     *  创建AuthContext
     * @param joinPoint
     * @return
     */
    private AuthContext setAuthzContext(JoinPoint joinPoint) {
        Authentication authentication = Authentication.current().orElseThrow(UnauthorizedException::new);
        MethodInterceptorHolder holder = createContext(joinPoint);
        MethodInterceptorContext paramContext = holder.createParamContext();
        AuthContext context = new AuthContext(authentication, paramContext);
        return context;
    }

    /**
     * 组装参数获取器
     *
     * @param joinPoint
     * @return
     */
    private MethodInterceptorHolder createContext(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        String id = DigestUtils.md5DigestAsHex(String.valueOf(method.hashCode()).getBytes());
        String[] argNames = MethodInterceptorHolder.NAME_DISCOVERER.getParameterNames(method);
        Object[] args = joinPoint.getArgs();
        Map<String, Object> argMap = new LinkedHashMap<>();
        for (int i = 0, len = args.length; i < len; i++) {
            argMap.put(argNames == null || argNames[i] == null ? "arg" + i : argNames[i], args[i]);
        }
        return new MethodInterceptorHolder(id, method, method.getClass(), argMap);
    }

    /**
     * 根据参数信息和uri信息进行进行权限判断，包括组装where条件和对vo对象的赋值
     *
     * @param context
     * @return
     */
    private boolean getAuthorized(AuthContext context, List<AuthResource> authResources) {
        // 有resource 继续进行dataScope DataAccess fileds的控制
        boolean authorized = dataAccessHandlers.stream().allMatch(h -> h.handle(context.getAuthentication(), authResources, null, context.getParamContext()));
        return authorized;
    }

    /**
     * 判断是否不需要过滤权限
     *
     * @param request
     * @return
     */
    private boolean urlFilter(HttpServletRequest request) {
        String url = request.getRequestURI().substring(request.getContextPath().length());
        String[] urls = authzSecurityProperties.getPermitAllUrl();
        return Arrays.asList(urls).contains(url);
    }
}
